#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <strings.h>
#include "err.hpp"
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <set>
#include <string>

using namespace std;

class tcpserver // 只关心读事件
{
    static const int defaultport = 8080; // 默认端口号
    static const int backlogSize = 10;   // TCP链接等待队列的大小

public:
    // 构造
    tcpserver(const int port = defaultport)
        : _listensock(-1), _port(port)
    {
    }

    // 析构
    ~tcpserver()
    {
        if (_listensock >= 0)
            close(_listensock);
    }

    // 创建套接字
    void Create()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
            exit(SOCKET_CREATE_FAILED);
    }

    // 绑定套接字
    void Bind()
    {
        // 1、初始化属性信息
        struct sockaddr_in infor;
        bzero(&infor, sizeof(infor));
        infor.sin_addr.s_addr = htonl(INADDR_ANY); // ip地址
        infor.sin_port = htons(_port);             // 端口号
        infor.sin_family = AF_INET;                // 通信方式
        // 2、绑定(成功返回0，失败返回-1)
        if(bind(_listensock, (struct sockaddr *)&infor, sizeof(infor)))
            exit(SOCKET_BIND_FAILED);
            
    }

    // 监听套接字
    void Listen()
    {
        if (listen(_listensock, backlogSize)) // 成功返回0，失败返回-1
            exit(SOCKET_LISTEN_FAILED);
    }

    // 初始化服务器
    void init()
    {
        // 1、创建套接字
        Create();
        // 2、绑定套接字
        Bind();
        // 3、监听套接字
        Listen();
    }

    // 填充文件描述符
    void fill_fds(fd_set *readfds, fd_set *writefds, fd_set *exceptfds)
    {
        if (readfds || writefds || exceptfds) // 至少需要填充一个集合
        {
            for (const auto &fd : _fds)
            {
                if (readfds)
                    FD_SET(fd, readfds);
                if (writefds)
                    FD_SET(fd, writefds);
                if (exceptfds)
                    FD_SET(fd, exceptfds);
            }
        }
    }

    // 获取新连接
    void Accept()
    {
        // 1、获取一个新连接(至少有一个)
        struct sockaddr_in clientinfor;
        socklen_t size = sizeof(clientinfor);
        bzero(&clientinfor, sizeof(clientinfor));
        int sock = accept(_listensock, (struct sockaddr *)&clientinfor, &size);
        // 2、判断是否获取成功
        if (sock < 0)
        {
            cout << "accept failed" << endl;
            exit(SOCK_ACCEPT_FAILED);
        }

        // 2、获取新连接成功，读取客户端信息
        string clientip;
        uint16_t clientport;
        clientip = inet_ntoa(clientinfor.sin_addr); // 将网络32位ip地址转化成本地字符串形式
        clientport = ntohs(clientinfor.sin_port);
        cout << "get a new connect,clientip:" << clientip << ",clientport:" << clientport << endl;

        // 3、将新链接添加到文件描述符集合中
        _fds.insert(sock);
    }

    //读取数据
    void Receive(int sock)
    {
        char buffer[1024];
        int n = recv(sock, buffer, sizeof(buffer) - 1,0);

        if (n > 0) // 读取成功
        {
            buffer[n] = 0; // 在有效数据后添加一个\0
            cout << "received a message:" << buffer << endl;
            // 再将收到的数据原路写回
            send(sock, buffer, n,0);
        }
        else if (n == 0) // 写端文件描述符已关闭
        {
            // 先将sock从文件描述符集合中移除，再关闭sock
            _fds.erase(sock);
            close(sock);
            cout << "client quit" << endl;
        }
        else // 读取出错
        {
            cout << "recv err" << endl;
            // 先将sock从文件描述符集合中移除，再关闭sock
            _fds.erase(sock);
            close(sock);
            exit(RECV_ERR);
        }
    }

    // 启动服务器(只处理读事件)
    void start()
    {
        // 1、先将监听套接字存进_fds列表中
        _fds.insert(_listensock);

        // 2、阻塞式等待所有的文件描述符，然后依次处理读事件就绪的文件描述符
        while (true)
        {
            // 1、将所有文件描述符添加进读事件集合中，只关心读事件
            fd_set readfds;
            fill_fds(&readfds, nullptr, nullptr);

            // 2、获取当前最大的文件描述符
            int maxfd = -1;
            if (_fds.empty()) // 没有需要监视的文件描述符
                continue;

            // 有需要监视的文件描述符
            maxfd = *_fds.rbegin();

            // 3、监视 fd_set集合中的所有文件描述符
            struct timeval timeout = {1, 0}; // 等待时间设置为1秒
            int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);

            // 4、处理就绪事件
            if (n > 0) // 有n个文件描述符就绪，处理所有就绪的文件描述符
            {
                for (const auto &fd : _fds)
                {
                    if (FD_ISSET(fd, &readfds)) // fd文件描述符的读事件就绪了
                    {
                        if (fd == _listensock) // 监听描述符就绪，获取新连接
                            Accept();          //
                        else                   // 其他链接的文件描述符就绪，读取数据
                            Receive(fd);
                    }
                }
            }
            else if (n == 0) // 没有文件描述符就绪，超时返回
            {
                cout << "timeout..." << endl;
            }
            else // 等待出错
            {
                cout << "select failed" << endl;
                exit(SELECT_FAILED);
            }

            sleep(1);
        }
    }

private:
    // 禁止拷贝构造
    tcpserver(const tcpserver &) = delete;

    // 禁止赋值重载
    tcpserver &operator=(const tcpserver &) = delete;

private:
    int _listensock; // 监听描述符
    int _port;       // 端口号
    set<int> _fds;   // 文件描述符列表
};